Zhihao's Studio.

D3 on Angular(2)

Word count: 1,560 / Reading time: 7 min
2014/11/15 Share

More Dynamic visualizations

上一篇博客介绍了如何借助Angular来完成一个指令,达到可复用目的,但是似乎依然不够动态,我们不得不提前输入想要展示的数组。
Angular的最大的特点是双向数据绑定,我们能否借助这个特性来完成更动态的可视化图呢?让Angular帮我们盯着数据,一旦数据改变了,就更新DOM元素。这一切都是Angular帮我们完成的,我们唯一需要做的是指定数据和DOM元素之间的关系,不需要写很多的代码。

双向数据绑定

先来用一个简单的例子来展示一下Angular帮我们完成的双向数据绑定,这里用一个简单的例子来展示一下。

1
2
3
4
5
6
7
8
//\<body ng-app\>
//\<input type="range" ng-model="ourRangeValue"\>
//\<input type="range" ng-model="ourRangeValue"\>
// \<input type="range" ng-model="ourRangeValue"\>
//\<input type="range" ng-model="ourRangeValue"\>
//\</body\>

上面的例子展示了四根sliders,当我们任意滑动一个slider,其他的三个也会跟着一起动。
这种自动更新发生在叫做scope的变量中,上面的例子中,ng-model指令在scope创建了一个叫ourRangeValue的变量,而且当ourRangeValue改变的时候,会处理\的更新。如果我们在一个新的DOM元素上创建一个scope,它所有的子DOM元素都可以拿这个scope,最顶层的scope是root scope。最简单的创建scope的方法是创建一个controller(控制器)。

两种变体

变体1:

1
2
3
4
5
6
7
8
9
10
11
12
//\<body ng-app="myApp"\>
// \<div ng-controller="HelloController"\>
// \<input type="range" ng-model="ourRangeValue"\>
// \<input type="range" ng-model="ourRangeValue"\>
// \</div\>
// \<div ng-controller="HelloController"\>
// \<input type="range" ng-model="ourRangeValue"\>
// \<input type="range" ng-model="ourRangeValue"\>
// \</div\>
//\</body\>

变体2:

1
2
3
4
5
6
7
8
9
10
11
12
//\<body ng-app="myApp" ng-init="foobar = { value: 50 }"\>
// \<div ng-controller="HelloController"\>
// \<input type="range" ng-model="foobar.value"\>
// \<input type="range" ng-model="foobar.value"\>
// \</div\>
// \<div ng-controller="HelloController"\>
// \<input type="range" ng-model="foobar.value"\>
// \<input type="range" ng-model="foobar.value"\>
// \</div\>
// \</body\>

这两种变体格各自的结果是什么,这应该是Angular的基础知识,大家可以自己去尝试一下。第一种情况改变第一个HelloController的foobar时候,会创建一个新的scope1.foobar替代rootScope.foobar。而第二种情况,是使用了另一个object包装了一下,这样就可以避免覆盖掉父scope的属性。这样我们并没有指定一个新的值给foobar,所以子scope不需要重新创建新的object。

Make visualizations dynamic with $watch

$scope.watch

使用Angular内置的指令使得绑定一个值的变化到另一个视图中的变化变得异常简单,不过对于稍微复杂点的可视化元素,我们就需要剪子自己的指令并监听scope内值的变化并且更新他们的内容。Angular里监听的函数是$scope.watch函数。
下面展示一个简单的例子:

1
2
3
4
\<!-- scope.$watch('progress',function(progress){
rect.attr({x: 0, y: 0, width: width \* progress / 100, height: height }); }); --\>

每次当slider被拖动,下面的进度条会跟着动,因为此时scope.$watch()函数被触发了。

一个更复杂的例子

我们将使用上一篇博客中使用的donut chart的例子。一种错误的做法:

1
2
3
4
5
6
7
8
9
10
//\<body ng-app="myApp" ng-init="chart=[10, 20, 30]()"\>
// \<input type="range" ng-model="chart[0]()"\>
// \<br\>
// \<input type="range" ng-model="chart[1]()"\>
// \<br\>
// \<input type="range" ng-model="chart[2]()"\>
// \<br\>
// \<donut-chart data="chart"\>\</donut-chart\>

好像没有按照我们预想的去做,因为没有watchdata scope变量的变化,正确的做法是:

1
2
3
4
5
6
7
\<!-- scope.$watch('data',function(data){
console.log("an element within `data` changed!");
console.log(data);
},true);
--\>

一个很大的不同是我们$watch函数的第三个参数是true,true告诉了watch函数,如果数组里面的值发生了改变也会触发,false不会触发,因为数组还是3个值,它从某种意义上来说并没变。
完整的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//\<!DOCTYPE html\>
//\<html\>
//\<head\>
// \<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.min.js"\>\</script\>
// \<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"\>\</script\>
//\</head\>
//\<body ng-app="myApp" ng-init="chart=[10, 20, 30]()"\>
// \<input type="range" ng-model="chart[0]()"\>
// \<br\>
// \<input type="range" ng-model="chart[1]()"\>
// \<br\>
// \<input type="range" ng-model="chart[2]()"\>
// \<br\>
// \<donut-chart data="chart"\>\</donut-chart\>
// \<script\>
// var myApp = angular.module('myApp', []());
// myApp.directive('donutChart', function(){
// function link(scope, el, attr){
// var color = d3.scale.category10();
// var data = scope.data;
// var width = 300;
// var height = 300;
// var min = Math.min(width, height);
// var svg = d3.select(el[0]()).append('svg');
// var pie = d3.layout.pie().sort(null);
// var arc = d3.svg.arc()
// .outerRadius(min / 2 \* 0.9)
// .innerRadius(min / 2 \* 0.5);
//
// svg.attr({width: width, height: height});
// var g = svg.append('g')
// // center the donut chart
// .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
//
// // add the \<path\>s for each arc slice
// var arcs = g.selectAll('path').data(pie(data))
// .enter().append('path')
// .style('stroke', 'white')
// .attr('fill', function(d, i){ return color(i) });
//
// scope.$watch('data', function(){
// arcs.data(pie(data)).attr('d', arc);
// }, true);
// }
// return {
// link: link,
// restrict: 'E',
// scope: { data: '=' }
// };
// });
// \</script\>
//\</body\>
//\</html\>

我们的Donut chart看起来不错,会随着slider的变化而发生变化了,但是一直只有三个slider,这稍微有点不爽。下面我们就增加两个按钮,一个是增加一个变量,一个是减少一个变量。其实呢,就是给chart数组里push一个数或者pop出一个数。而为了让slider的数目也随着数组的变化而变化,这就需要借助ng-repeat指令。
\
至此我们完成了所有的任务,完整代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
//\<!DOCTYPE html\>
//\<html\>
//\<head\>
// \<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.10/angular.js"\>\</script\>
// \<script src="http://d3js.org/d3.v3.js" charset="utf-8"\>\</script\>
//\</head\>
//\<body ng-app="myApp" ng-init="chart=[{value: 10}, {value: 20}, {value: 30}]()"\>
// \<donut-chart data="chart" style="float:right"\>\</donut-chart\>
// \<button ng-click="chart.push({value: 10})"\>add slice\</button\>
// \<button ng-click="chart.pop()"\>remove slice\</button\>
// \<input type="range" ng-model="slice.value" ng-repeat="slice in chart track by $index"\>
// \<script\>
// var myApp = angular.module('myApp', []());
// myApp.directive('donutChart', function(){
// function link(scope, el, attr){
// var color = d3.scale.category10();
// var width = 200;
// var height = 200;
// var min = Math.min(width, height);
// var svg = d3.select(el[0]()).append('svg');
// var pie = d3.layout.pie().sort(null);
// var arc = d3.svg.arc()
// .outerRadius(min / 2 \* 0.9)
// .innerRadius(min / 2 \* 0.5);
//
// pie.value(function(d){ return d.value; });
//
// svg.attr({width: width, height: height});
// var g = svg.append('g')
// // center the donut chart
// .attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
//
// // add the \<path\>s for each arc slice
// var arcs = g.selectAll('path');
//
// scope.$watch('data', function(data){
// arcs = arcs.data(pie(data));
// arcs.enter().append('path')
// .style('stroke', 'white')
// .attr('fill', function(d, i){ return color(i) });
// arcs.exit().remove();
// arcs.attr('d', arc);
// }, true);
// }
// return {
// link: link,
// restrict: 'E',
// scope: { data: '=' }
// };
// });
// \</script\>
//\</body\>
//\</html\>

基础知识就介绍到这里,下面会介绍一个稍微难一些的例子。

CATALOG
  1. 1. More Dynamic visualizations
  2. 2. 双向数据绑定
    1. 2.1. 两种变体
  3. 3. Make visualizations dynamic with $watch
    1. 3.1. $scope.watch
    2. 3.2. 一个更复杂的例子